package com.lipaluma.util;

import java.lang.reflect.Method;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Locale;

import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.joda.time.format.DateTimeFormat;

import com.lipaluma.exceptions.CsvParserProcessException;

public class ParserUtils {

	private enum TypeEnum {
		STRING {
			@Override
			public Object parse(String value, String pattern) {
				return value;
			}
			@Override
			public Object parse(String value, String pattern, Locale locale) {
				return value;
			}
		}, LONG {
			@Override
			public Object parse(String value, String pattern) {
				try {
					String valueParsed = parseNumeric(value, pattern);
					return new Long(valueParsed);
				} catch (NumberFormatException e) {
					return parseInDouble(value, "value "+value+" cannot be parsed to a long", e).longValue();
				}
			}
			@Override
			public Object parse(String value, String pattern, Locale locale) {
				if(locale == null)
					return parse(value, pattern);
				try {
					String valueParsed = parseNumeric(value, pattern, locale);
					return new Long(valueParsed);
				} catch (NumberFormatException e) {
					return parseInDouble(value, "value "+value+" cannot be parsed to a long", e).longValue();
				}
			}
		}, INTEGER {
			@Override
			public Object parse(String value, String pattern) {
				try {
					String valueParsed = parseNumeric(value, pattern);
					return new Integer(valueParsed);
				} catch (NumberFormatException e) {
					return parseInDouble(value, "value "+value+" cannot be parsed to an integer", e).intValue();
				}
			}
			@Override
			public Object parse(String value, String pattern, Locale locale) {
				if(locale == null)
					return parse(value, pattern);
				try {
					String valueParsed = parseNumeric(value, pattern, locale);
					return new Integer(valueParsed);
				} catch (NumberFormatException e) {
					return parseInDouble(value, "value "+value+" cannot be parsed to an integer", e).intValue();
				}
			}
		}, INT {
			@Override
			public Object parse(String value, String pattern) {
				try {
					String valueParsed = parseNumeric(value, pattern);
					return new Integer(valueParsed);
				} catch (NumberFormatException e) {
					return parseInDouble(value, "value "+value+" cannot be parsed to an int", e).intValue();
				}
			}
			@Override
			public Object parse(String value, String pattern, Locale locale) {
				if(locale == null)
					return parse(value, pattern);
				try {
					String valueParsed = parseNumeric(value, pattern, locale);
					return new Integer(valueParsed);
				} catch (NumberFormatException e) {
					return parseInDouble(value, "value "+value+" cannot be parsed to an int", e).intValue();
				}
			}
		}, SHORT {
			@Override
			public Object parse(String value, String pattern) {
				try {
					String valueParsed = parseNumeric(value, pattern);
					return new Short(valueParsed);
				} catch (NumberFormatException e) {
					return parseInDouble(value, "value "+value+" cannot be parsed to a short", e).shortValue();
				}
			}
			@Override
			public Object parse(String value, String pattern, Locale locale) {
				if(locale == null)
					return parse(value, pattern);
				try {
					String valueParsed = parseNumeric(value, pattern, locale);
					return new Short(valueParsed);
				} catch (NumberFormatException e) {
					return parseInDouble(value, "value "+value+" cannot be parsed to a short", e).shortValue();
				}
			}
		}, DOUBLE {
			@Override
			public Object parse(String value, String pattern) {
				try {
					String valueParsed = parseNumeric(value, pattern);
					return new Double(valueParsed);
				} catch (NumberFormatException e) {
					throw new CsvParserProcessException("value "+value+" cannot be parsed to a double", e);
				}
			}
			@Override
			public Object parse(String value, String pattern, Locale locale) {
				if(locale == null)
					return parse(value, pattern);
				try {
					String valueParsed = parseNumeric(value, pattern, locale);
					return new Double(valueParsed);
				} catch (NumberFormatException e) {
					throw new CsvParserProcessException("value "+value+" cannot be parsed to a double", e);
				}
			}
		}, FLOAT {
			@Override
			public Object parse(String value, String pattern) {
				try {
					String valueParsed = parseNumeric(value, pattern);
					return new Float(valueParsed);
				} catch (NumberFormatException e) {
					throw new CsvParserProcessException("value "+value+" cannot be parsed to a float", e);
				}
			}
			@Override
			public Object parse(String value, String pattern, Locale locale) {
				if(locale == null)
					return parse(value, pattern);
				try {
					String valueParsed = parseNumeric(value, pattern, locale);
					return new Float(valueParsed);
				} catch (NumberFormatException e) {
					throw new CsvParserProcessException("value "+value+" cannot be parsed to a float", e);
				}
			}
		}, BOOLEAN {
			@Override
			public Object parse(String value, String pattern) {
				return BooleanUtils.toBoolean(value);
			}
			@Override
			public Object parse(String value, String pattern, Locale locale) {
				return BooleanUtils.toBoolean(value);
			}
		}, DATE {
			@Override
			public Object parse(String value, String pattern) {
				if(StringUtils.isBlank(pattern))
					throw new CsvParserProcessException("Error on parsing date : "+value+". The pattern was empty.");
				try {
					SimpleDateFormat fmt = new SimpleDateFormat(pattern);
					return fmt.parse(value);
				} catch (ParseException e) {
					throw new CsvParserProcessException("Error on parsing date : "+value+" with pattern "+pattern, e);
				}
			}

			@Override
			public Object parse(String value, String pattern, Locale locale) {
				if(locale == null)
					return parse(value, pattern);
				if(StringUtils.isBlank(pattern))
					throw new CsvParserProcessException("Error on parsing date : "+value+". The pattern was empty.");
				try {
					SimpleDateFormat fmt = new SimpleDateFormat(pattern, locale);
					return fmt.parse(value);
				} catch (ParseException e) {
					throw new CsvParserProcessException("Error on parsing date : "+value+" with pattern "+pattern, e);
				}
			}
		}, DATETIME {
			@Override
			public Object parse(String value, String pattern) {
				if(StringUtils.isBlank(pattern))
					throw new CsvParserProcessException("Error on parsing date : "+value+". The pattern was empty.");
				return DateTimeFormat.forPattern(pattern).parseDateTime(value);
			}

			@Override
			public Object parse(String value, String pattern, Locale locale) {
				if(locale == null)
					return parse(value, pattern);
				if(StringUtils.isBlank(pattern))
					throw new CsvParserProcessException("Error on parsing date : "+value+". The pattern was empty.");
				return DateTimeFormat.forPattern(pattern).withLocale(locale).parseDateTime(value);
			}
		};

		private static String parseNumeric(String value, String pattern) {
			if(StringUtils.isNotBlank(pattern)) {
				DecimalFormat format = new DecimalFormat(pattern);
				try {
					return format.parse(value).toString();
				} catch (ParseException e) {
					throw new NumberFormatException("Error on parsing "+value+" like "+pattern+" : " + e.getMessage());
				}
			}
			return value;
		}

		private static String parseNumeric(String value, String pattern, Locale locale) {
			if(StringUtils.isNotBlank(pattern)) {
				DecimalFormat format = new DecimalFormat(pattern, new DecimalFormatSymbols(locale));
				try {
					return format.parse(value).toString();
				} catch (ParseException e) {
					throw new NumberFormatException("Error on parsing "+value+" like "+pattern+" : " + e.getMessage());
				}
			}
			return value;
		}
	
		private static Double parseInDouble(String value, String errorMessage, Exception e) {
			try {
				return new Double(value);
			} catch (NumberFormatException ex) {
				throw new CsvParserProcessException(errorMessage, e);
			}
		}
	
		public abstract Object parse(String value, String pattern);

		public abstract Object parse(String value, String pattern, Locale locale);
	
	}

	/**
	 * parse the String given value to the given class
	 * @param clazz the target class to parse the value
	 * @param value the value to parse
	 * @return the value parsed
	 */
	public static Object parseToType(Class<?> clazz, String value) {
		return parseToType(clazz, value, null);
	}

	/**
	 * parse the String given value to the given class with optionally the given pattern
	 * @param clazz the target class to parse the value
	 * @param value the value to parse
	 * @param pattern used to parse the value. If null, no pattern will be used.
	 * @return the value parsed
	 */
	public static Object parseToType(Class<?> clazz, String value, String pattern) {
		TypeEnum parser = TypeEnum.valueOf(clazz.getSimpleName().toUpperCase());
		return parser.parse(value, pattern);
	}

	public static Object parseToType(Class<?> clazz, String value, String pattern, Locale locale) {
		TypeEnum parser = TypeEnum.valueOf(clazz.getSimpleName().toUpperCase());
		return parser.parse(value, pattern, locale);
	}
	/**
	 * parse the value to the given enum clazz
	 * @param clazz enum class to parse the value
	 * @param value the value to parse
	 * @return the value parsed
	 */
	public static Enum<?> parseToEnum(Class<?> clazz, String value) {
		try {
			Method method = clazz.getDeclaredMethod("valueOf", String.class);
			ReflectionUtils.makeAccessible(method);
			return (Enum<?>)ReflectionUtils.invokeMethod(method, null, value);
		} catch (NoSuchMethodException e) {
			throw new CsvParserProcessException("The type "+clazz.getName()+" is not an enum.", e);
		}		
	}

	/**
	 * parse the String given value to the given class managing enums or types
	 * @param clazz the target class to parse the value
	 * @param value the value to parse
	 * @param pattern used to parse the value. If null, no pattern will be used.
	 * @return the value parsed
	 */
	public static Object parseToTypeOrEnum(Class<?> clazz, String value, String pattern) {
		if (clazz.isEnum()) {
			return parseToEnum(clazz, value);
		} else { 
			return parseToType(clazz, value, pattern);
		}
	}

}
